<?php
namespace Zwei\Retry;

use Zwei\Retry\Exception\RetryException;

class Retry {
    /**
     * @var bool 是否调用，调用过的不再执行
     */
    protected $isCalled = false;

    /**
     * @var bool 是否调用成功，调用成功的不再重试
     */
    protected $isCalledSuccess = false;
    /**
     * @var int 最大重试次数
     */
    protected $maxRetry = 3;

    /**
     * @var int 重试间隔微秒数
     * 2^重试次数 * $delay
     */
    protected $delay = 0;

    /**
     * @var int 当前重试次数
     */
    protected $currentRetry = 0;

    /**
     * @var mixed 结果
     */
    protected $result = null;

    /**
     * @var array 重试异常列表
     */
    protected $exceptions = [];

    protected $startTime = 0;
    protected $endTime = 0;

    /**
     * @param int $maxRetry 最大重试次数
     * @param int $delayMicroseconds 重试间隔微秒数（2^重试次数 * $delay）
     */
    public function __construct($maxRetry = 3, $delayMicroseconds = 5000)
    {
        $this->maxRetry = $maxRetry;
        $this->delay = $delayMicroseconds;
    }

    /**
     * @param bool $isCalled
     * @param bool $isCalledSuccess
     * @param mixed $result
     * @return $this
     */
    public function setIsCalled($isCalled, $isCalledSuccess, $result)
    {
        $this->isCalled = $isCalled ? true : false;
        $this->isCalledSuccess = $isCalledSuccess ? true : false;
        $this->result = $result;
        return $this;
    }

    /**
     * @param callable $callFunc 需要重试的函数
     * @param bool $isFirstSleep 是否第一次休眠，默认第一次休眠
     * @param mixed ...$args 函数参数
     * @return mixed|null callFunc 返回值
     */
    public function execute(callable $callFunc, $isFirstSleep = true, ...$args)
    {
        if ($this->isCalled) {
            return $this->getResult();
        }

        $this->isCalled = true;
        $this->startTime = microtime(true);
        while ($this->currentRetry < $this->maxRetry) {
            $this->currentRetry++;
            try {
                if ($this->delay > 0) {
                    if (($this->getCurrentRetry() != 1) || ($this->getCurrentRetry() == 1 && $isFirstSleep)) {
                        $sleep = pow(2, $this->currentRetry) * $this->delay;
                        usleep($sleep);
                    }

                }
                $this->result = call_user_func_array($callFunc, $args);
                $this->isCalledSuccess = true;
                break;
            } catch (RetryException $e) {
                $this->exceptions[] = $e;
            }
        }
        $this->endTime = microtime(true);
        return $this->getResult();
    }

    public function isCalled()
    {
        return $this->isCalled;
    }

    public function isCalledSuccess()
    {
        return $this->isCalledSuccess;
    }

    public function getMaxRetry()
    {
        return $this->maxRetry;
    }

    public function getDelay()
    {
        return $this->delay;
    }

    public function getCurrentRetry()
    {
        return $this->currentRetry;
    }

    public function getResult()
    {
        return $this->result;
    }

    /**
     * @return []\Exception
     */
    public function getExceptions()
    {
        return $this->exceptions;
    }

    /**
     * @return null|\Exception
     */
    public function getFirstException()
    {
        if (!isset($this->exceptions[0])) {
            return null;
        }
        return $this->exceptions[0];
    }

    /**
     * @return null|\Exception
     */
    public function getLastException()
    {
        return end($this->exceptions);
    }

    /**
     * @return int
     */
    public function getDelayTotalSeconds()
    {
        return $this->endTime - $this->startTime;
    }

    /**
     * @return bool
     */
    public function isRetry() {
        if (!$this->isRetry()) {
            return false;
        }
        if ($this->getCurrentRetry() < 1) {
            return false;
        }
        return true;
    }

    public static function newInstance($maxRetry = 3, $delayMicroseconds = 10)
    {
        return new Retry($maxRetry, $delayMicroseconds);
    }

    public static function throwRetryException($message, $code = 0)
    {
        throw new RetryException($message, $code);
    }

    public function toArray()
    {
        return [
            'isCalled' => $this->isCalled,
            'isCalledSuccess' => $this->isCalledSuccess,
            'maxRetry' => $this->maxRetry,
            'delay' => $this->delay,
            'currentRetry' => $this->currentRetry,
            'result' => $this->result,
            'exceptions' => $this->exceptions,
            'startTime' => $this->startTime,
            'endTime' => $this->endTime,
        ];
    }
}